from logging import getLogger

import zope.interface
from zope.interface.exceptions import DoesNotImplement

from lxml import etree

from sqlalchemy import MetaData
from sqlalchemy.schema import Column, ForeignKey, Table
from sqlalchemy.types import Integer, Unicode, DateTime, TypeDecorator
from sqlalchemy.orm import relation, join
from sqlalchemy.sql.expression import asc,desc

from meercat.base.metadata import Metadata
from meercat.interfaces import IInstance


log = getLogger('meercat.harvester.voyager.declarative')

__all__ = ['createClasses']

NAMESPACES = {'rdf':'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
              'frbr':'http://vocab.org/frbr/core',
              'dc':'http://purl.org/dc/terms/'}


class ConfigurableUnicode(TypeDecorator):
    def __init__(self, length=None, encoding_errors='strict', **kwargs):
        self.encoding_errors = encoding_errors
        self.impl = Unicode(length=length, **kwargs)
    
    def convert_bind_param(self, value, engine):
        return value

    def convert_result_value(self, value, engine):
        if self.impl.convert_unicode or engine.convert_unicode:
            if value is not None and not isinstance(value, unicode):
                return value.decode(engine.encoding, self.encoding_errors)
        return value
    
    def result_processor(self, dialect):
        def process(value):
            if value is not None and not isinstance(value, unicode):
                return value.decode(dialect.encoding, self.encoding_errors)
            else:
                return value
        return process

def createClasses(harvester):
    source = u'voyager://%s' % (harvester.getSource(),)
    Base = harvester.getDeclarativeBase()
    
    tables = {}
    
    tables['bib_mfhd'] = Table('bib_mfhd',Base.metadata,
        Column('bib_id', Integer, ForeignKey('bib_master.bib_id'),
               primary_key=True),
        Column('mfhd_id', Integer, ForeignKey('mfhd_master.mfhd_id'),
               primary_key=True))
    
    class VoyagerLibrary(Base):
        """ORM mapped class for Voyager library """
        __tablename__ = 'library'
        
        id = Column('library_id',Integer, primary_key=True)
        short_name = Column('library_name',Unicode(50))
        display_name = Column('library_display_name',Unicode(80))
        nuc_code = Column('nuc_code',Unicode(15))
        
        def getXML(self):
            attrib = {'{%s}about' % (NAMESPACES['rdf'],):
                          '%s/library/%s' % (source, str(self.id)),
                      'type': u'library'}
            
            library = etree.Element('{%s}Location' % (NAMESPACES['dc'],),
                                    attrib=attrib, nsmap=NAMESPACES)
            
            id = etree.Element('{%s}identifier' % (NAMESPACES['dc'],),
                               attrib={'type':u'voyagerLibrary'})
            id.text = str(self.id)
            library.append(id)
            
            name = etree.SubElement(library,
                                    '{%s}title' % (NAMESPACES['dc'],),
                                    attrib={'type':u'internal'})
            name.text = self.short_name
            name = etree.SubElement(library,
                                    '{%s}title' % (NAMESPACES['dc'],),
                                    attrib={'type':u'display'})
            name.text = self.display_name
            
            return library
    
    class VoyagerLocation(Base):
        """ORM mapped class for Voyager locations """
        
        __tablename__ = 'location'
        
        id = Column('location_id', Integer, primary_key=True)
        code = Column('location_code', Unicode(10))
        name = Column('location_name', Unicode(25))
        display_name = Column('location_display_name', Unicode(60))
        spine_label = Column('location_spine_label', Unicode(25))
        opac = Column('location_opac', Unicode(1))
        suppress = Column('suppress_in_opac', Unicode(1))
        library_id = Column('library_id', Integer, ForeignKey('library.library_id'))
        mfhd_count = Column('mfhd_count', Integer)
        library = relation(VoyagerLibrary, lazy=False,
                           primaryjoin=library_id==VoyagerLibrary.id)
        
        def suppressed(self):
            return self.suppress.lower() == 'y'
        
        def getXML(self,attrib={}):
            if self.suppressed():
                return []
            
            data = []
            
            attrib.setdefault('{%s}about' % (NAMESPACES['rdf'],),
                              '%s/location/%s' % (source,self.id))
            
            location = etree.Element('{%s}Location' % (NAMESPACES['dc'],),
                                     attrib=attrib, nsmap=NAMESPACES)
            
            data.append(location)
            
            id = etree.Element('{%s}identifier' % (NAMESPACES['dc'],),
                               attrib={'type':u'voyagerLocation'})
            id.text = str(self.id)
            location.append(id)
            
            if self.library:
                location.append(self.library.getXML())
            
            if self.code:        
                code = etree.SubElement(location,
                                        '{%s}title' % (NAMESPACES['dc'],),
                                        attrib={'type':'code'})
                code.text = self.code
            
            if self.name:
                name = etree.SubElement(location,
                                        '{%s}title' % (NAMESPACES['dc'],),
                                        attrib={'type':'internal'})
                name.text = self.name
            
            if self.display_name:
                name = etree.SubElement(location,
                                        '{%s}title' % (NAMESPACES['dc'],))
                name.text = self.display_name
            
            if self.spine_label:
                spine_label = etree.Element('{%s}title' % (NAMESPACES['dc'],),
                                            attrib={'type':'spine label'},
                                            nsmap=NAMESPACES)
                spine_label.text = self.spine_label
                location.append(spine_label)
            
            return data
    
    class VoyagerHoldingData(Base):
        """ORM mapped class for Voyager holdings data (MARC records)"""
        
        __tablename__ = 'mfhd_data'
        mfhd_id = Column('mfhd_id', Integer, ForeignKey('mfhd_master.mfhd_id'),
                         primary_key=True)
        seqnum = Column('seqnum', Integer,primary_key=True)
        data = Column('record_segment',
                      ConfigurableUnicode(990,encoding_errors='replace'))
    
    class VoyagerItemStatusType(Base):
        __tablename__ = 'item_status_type'
        
        type =  Column('item_status_type', Integer, primary_key=True)
        description = Column('item_status_desc', Unicode(25)) 
    
    class VoyagerItemStatus(Base):
        """ORM mapped class for Voyager item status """
        
        __tablename__ = 'item_status'
        
        item_id = Column('item_id', Integer, ForeignKey('item.item_id'),
                         primary_key=True)
        type_id = Column('item_status', Integer,
                           ForeignKey('item_status_type.item_status_type'),
                           primary_key=True)
        date = Column('item_status_date', DateTime)
        
        type = relation(VoyagerItemStatusType, lazy=False,
                        primaryjoin=type_id==VoyagerItemStatusType.type)
        
        def getXML(self):
            status = etree.Element(u'status',
                                   attrib={'{%s}about' % (NAMESPACES['rdf'],):
                                           '%s/status/%s' % 
                                               (source, str(self.type_id),)},
                                   nsmap=NAMESPACES)
            e = etree.SubElement(status,
                                 u'{%s}description' % (NAMESPACES['dc'],))
            e.text = self.type.description
            e = etree.SubElement(status,
                                 u'{%s}identifier' % (NAMESPACES['dc'],),
                                 type=u'voyagerItemStatus')
            e.text = str(self.type_id)
            
            if self.date:
                e = etree.SubElement(status,u'{%s}date' % (NAMESPACES['dc'],))
                e.text = str(self.date)
            
            return status
    
    class VoyagerItem(Base):
        """ORM mapped class for Voyager items """
        __tablename__ = 'item'
        
        id = Column('item_id', Integer, primary_key=True)
        perm_location_id = Column('perm_location', Integer,
                                  ForeignKey('location.location_id'))
        temp_location_id = Column('temp_location', Integer,
                                  ForeignKey('location.location_id'))
        item_type_id = Column('item_type_id', Integer)
        temp_item_type_id = Column('temp_item_type_id', Integer)
        copy_number = Column('copy_number', Integer)
        on_reserve = Column('on_reserve', Unicode(1))
        reserve_charges = Column('reserve_charges', Integer)
        pieces = Column('pieces', Integer)
        price = Column('price', Integer)
        spine_label = Column('spine_label', Unicode(25))
        historical_charges = Column('historical_charges', Integer)
        historical_browses = Column('historical_browses', Integer)
        recalls_placed = Column('recalls_placed', Integer)
        holds_placed = Column('holds_placed', Integer)
        created = Column('create_date', DateTime)
        modified = Column('modify_date', DateTime)
        create_operator_id = Column('create_operator_id', Integer)
        modifiy_operator_id = Column('modify_operator_id', Integer)
        create_location_id = Column('create_location_id', Integer,
                                    ForeignKey('location.location_id'))
        modify_location_id = Column('modify_location_id', Integer,
                                    ForeignKey('location.location_id'))
        sequence_number = Column('item_sequence_number', Integer)
        historical_bookings = Column('historical_bookings', Integer)
        media_type_id = Column('media_type_id', Integer)
        short_loan_charges = Column('short_loan_charges', Integer)
        
        permanent_location = relation(VoyagerLocation,lazy=False,
                                      primaryjoin=perm_location_id==VoyagerLocation.id)
        temporary_location = relation(VoyagerLocation,lazy=False,
                                      primaryjoin=temp_location_id==VoyagerLocation.id)
        create_location = relation(VoyagerLocation,lazy=False,
                                   primaryjoin=create_location_id==VoyagerLocation.id)
        modify_location = relation(VoyagerLocation,lazy=False,
                                   primaryjoin=modify_location_id==VoyagerLocation.id)
        status = relation(VoyagerItemStatus, backref='item', lazy=False)
        
        def getXML(self):
            item = etree.Element('{%s}Item' % (NAMESPACES['frbr'],),
                                 attrib={'{%s}about' % (NAMESPACES['rdf'],):
                                         '%s/item/%s' % (source,
                                                         str(self.id),)},
                                 nsmap=NAMESPACES)
            
            id = etree.Element('{%s}identifier' % (NAMESPACES['dc'],),
                               attrib={'type':u'voyagerItem'})
            id.text = str(self.id)
            item.append(id)
            
            if self.temporary_location:
                for t in self.temporary_location.getXML(attrib={'type': 'temporary'}):
                    item.append(t)
            
            if self.permanent_location:
                for t in self.permanent_location.getXML(attrib={'type': 'permanent'}):
                    item.append(t)
            
            e = etree.SubElement(item,u'copy_number')
            e.text = str(self.copy_number)
            e = etree.SubElement(item,u'on_reserve')
            e.text = str(self.on_reserve.lower() == 'y')
            e = etree.SubElement(item,u'pieces')
            e.text = str(self.pieces)
            e = etree.SubElement(item,u'price')
            e.text = str(self.price)
            
            if self.spine_label:
                e = etree.SubElement(item,'{%s}title' % (NAMESPACES['dc'],),
                                     attrib={'type':'spine label'})
                e.text = self.spine_label
            
            if self.created:
                e = etree.SubElement(item,'{%s}date' % (NAMESPACES['dc'],),
                                     attrib={'type':'create'})
                e.text = str(self.created)
            
            if self.modified:
                e = etree.SubElement(item,'{%s}date' % (NAMESPACES['dc'],),
                                     attrib={'type':'modify'})
                e.text = str(self.modified)
            
            e = etree.SubElement(item,u'item_sequence')
            e.text = str(self.sequence_number)
            
            for status in self.status:
                item.append(status.getXML())
            
            stats = etree.SubElement(item,u'statistics')
            e = etree.SubElement(stats,u'reserve_charges')
            e.text = str(self.reserve_charges)
            e = etree.SubElement(stats,u'historical_charges')
            e.text = str(self.historical_charges)
            e = etree.SubElement(stats,u'historical_browses')
            e.text = str(self.historical_browses)
            e = etree.SubElement(stats,u'historical_bookings')
            e.text = str(self.historical_bookings)
            e = etree.SubElement(stats,u'short_loan_charges')
            e.text = str(self.short_loan_charges)
            e = etree.SubElement(stats,u'recalls_placed')
            e.text = str(self.recalls_placed)
            e = etree.SubElement(stats,u'holds_placed')
            e.text = str(self.holds_placed)
            
            if self.barcodes:
                map(lambda b: item.append(b.getXML()), self.barcodes)
            
            return item
    
    class VoyagerItemBarcodeStatus(Base):
        __tablename__ = 'item_barcode_status'
        
        type = Column('barcode_status_type', Integer, primary_key=True)
        description = Column('barcode_status_desc', Unicode(25))
    
    class VoyagerItemBarcode(Base):
        __tablename__ = 'item_barcode'
        
        item_id = Column('item_id', Integer, ForeignKey('item.item_id'))
        barcode = Column('item_barcode', Unicode(30), primary_key=True)
        status_type = Column('barcode_status', Integer,
                             ForeignKey('item_barcode_status.barcode_status_type'))
        status_date = Column('barcode_status_date', DateTime)
        
        item = relation(VoyagerItem, backref='barcodes', lazy=False,
                        primaryjoin=item_id==VoyagerItem.id)
        status = relation(VoyagerItemBarcodeStatus, lazy=False,
                          primaryjoin=status_type==VoyagerItemBarcodeStatus.type)
        
        def getXML(self):
            attr = {'type':u'barcode'}
            
            if self.status.description:
                attr['status'] = self.status.description
            
            root = etree.Element('{%s}identifier' % (NAMESPACES['dc'],),
                                 attrib=attr)
            root.text = str(self.barcode)
            
            return root
            
    
    class VoyagerHoldingItems(Base):
        __tablename__ = 'mfhd_item'
        
        mfhd_id = Column('mfhd_id', Integer, ForeignKey('mfhd_master.mfhd_id'),
                         primary_key=True)
        item_id = Column('item_id', Integer, ForeignKey('item.item_id'),
                         primary_key=True)
        enum = Column('item_enum', Integer)
        chron = Column('chron', Unicode(80))
        year = Column('year', Unicode(20))
        caption = Column('caption', Unicode(256))
        text = Column('freetext', Unicode(256))
        item = relation(VoyagerItem, backref='holdingsItems', lazy=False,
                        primaryjoin=item_id==VoyagerItem.id)
        
        def getXML(self):
            item = self.item.getXML()
            
            if self.caption:
                e = etree.SubElement(item,u'{%s}title' % (NAMESPACES['dc'],),
                                     attrib={'type':u'caption'})
                e.text = self.caption
            if self.chron:
                e = etree.SubElement(item,u'{%s}coverage' % (NAMESPACES['dc'],),
                                     attrib={'type':u'chron'})
                e.text = self.chron
            if self.enum:
                e = etree.SubElement(item,u'enum')
                e.text = self.enum
            if self.text:
                e = etree.SubElement(item,u'{%s}description' % (NAMESPACES['dc'],))
                e.text = self.text
            
            if self.year:
                e = etree.SubElement(item,u'{%s}coverage' % (NAMESPACES['dc'],),
                                     attrib={'type':u'year'})
                e.text = self.year
            
            return item
                
    
    class VoyagerHolding(Base):
        """ORM mapped class for Voyager holdings records """
        
        __tablename__ = 'mfhd_master'
        
        id = Column('mfhd_id', Integer,primary_key=True)
        location_id = Column('location_id', Integer, 
                             ForeignKey('location.location_id'))
        location = relation(VoyagerLocation,lazy=False,
                            primaryjoin=location_id==VoyagerLocation.id)
        call_no_type = Column('call_no_type', Unicode(1))
        normalized_call_no = Column('normalized_call_no', Unicode(300))
        display_call_no = Column('display_call_no', Unicode(300))
        suppress = Column('suppress_in_opac', Unicode(1))
        source_module = Column('source_module', Unicode(1))
        record_status = Column('record_status', Unicode(1))
        record_type = Column('record_type', Unicode(1))
        encoding_level = Column('encoding_level', Unicode(1))
        field_007 = Column('field_007', Unicode(23))
        field_008 = Column('field_008', Unicode(32))
        created = Column('create_date', DateTime)
        modified = Column('update_date', DateTime)
        export_ok = Column('export_ok', Unicode(1))
        export_ok_date = Column('export_ok_date', DateTime)
        export_ok_opid = Column('export_ok_opid', Unicode(10))
        export_date = Column('export_date', DateTime)
        marc = relation(VoyagerHoldingData, lazy=False,
                        primaryjoin=id==VoyagerHoldingData.mfhd_id)
        items = relation(VoyagerHoldingItems, lazy=False,
                         primaryjoin=id==VoyagerHoldingItems.mfhd_id)
        
        def suppressed(self):
            return self.suppress.lower() == 'y'
        
        def getMarcRecord(self):
            self.marc.sort(key=lambda s: s.seqnum)
            
            return unicode('','utf-8').join(segment.data for \
                                            segment in self.marc)
        
        def getXML(self):
            if self.suppressed():
                return None
            
            holding = etree.Element('{%s}BibliographicResource' % (NAMESPACES['dc'],),
                                    attrib={'type':'holding',
                                            '{%s}about' % (NAMESPACES['rdf'],):
                                                '%s/holding/%s' % (source,
                                                                   str(self.id))},
                                    nsmap=NAMESPACES)
            
            id = etree.Element('{%s}identifier' % (NAMESPACES['dc'],),
                               attrib={'type':u'voyagerMfhd'})
            id.text = str(self.id)
            holding.append(id)
            
            for d in self.location.getXML():
                holding.append(d)
            #TODO: Switch call numbers to a dc element of some kind (identifier?)
            if self.display_call_no:
                call_number = etree.SubElement(holding,
                                               u'{%s}identifier' % (NAMESPACES['dc'],),
                                               type='call number',
                                               format='display')
                call_number.text = unicode(self.display_call_no)
            
            if self.normalized_call_no:
                try:
                    call_number = etree.Element(u'{%s}identifier' % (NAMESPACES['dc'],),
                                               type='call number',
                                               format='normalized')
                    call_number.text = self.normalized_call_no
                    holding.append(call_number)
                except Exception, e:
                    log.debug('Error adding the normalized call number from a '
                               'holding (mfhd_id: %s): %s' % (str(self.id),
                                                              str(e)))
            
            self.items.sort(key=lambda h: h.enum)
            
            for item in self.items:
                holding.append(item.getXML())
            
            return holding
    
    class VoyagerBibData(Base):
        """ORM mapped class for Voyager bibliographic data (MARC records) """
        
        __tablename__ = 'bib_data'
        
        bib_id = Column('bib_id', Integer, ForeignKey('bib_master.bib_id'),
                        primary_key=True)
        seqnum = Column('seqnum', Integer,primary_key=True)
        data = Column('record_segment', ConfigurableUnicode(990,encoding_errors='replace'))
    
    class VoyagerBibRecord(Base):
        """This class is for ORM mapping to Voyager bibliographic records. Each 
        Voyager bibliographic record is represented by a single meercat instance.
        """
        zope.interface.implements(IInstance)
        
        __tablename__ = 'bib_master'
        
        id = Column('bib_id', Integer, primary_key=True)
        library_id = Column('library_id', Integer,
                            ForeignKey('library.library_id'))
        library = relation(VoyagerLibrary, lazy=False,
                           primaryjoin=library_id==VoyagerLibrary.id)
        holdings = relation(VoyagerHolding,secondary=tables['bib_mfhd'],
                            backref='bib_record', lazy=False)
        suppress = Column('suppress_in_opac', Unicode(1))
        created = Column('create_date', DateTime)
        modified = Column('update_date', DateTime)
        marc = relation(VoyagerBibData,lazy=False, 
                        primaryjoin=id==VoyagerBibData.bib_id)
        
        __metadata = None
        
        def __len__(self):
            return len(self.getMetadata())
            
        def __iter__(self):
            return self.getMetadata().__iter__()
        
        def __hash__(self):
            return hash((self.getSource(),self.getIdentifier(),))
        
        def __eq__(self, other):
            if not IInstance.providedBy(other):
                return False
            
            return (other.getIdentifier() == self.getIdentifier() and
                    other.getSource() == self.getSource())
        
        def isActive(self):
            """Is this instance active """
            return self.suppress.lower() != 'y'
        
        def getSource(self):
            return source
      
        def getIdentifier(self):
            """Return the identifier used on the source for this instance """
            return unicode(str(self.id),'utf-8')
        
        def getMetadata(self):
            """Return an iterable container of object that implement the 
            meercat.interfaces.IMetdata interface. 
            """
            
            if self.__metadata is not None:
                return self.__metadata
            
            self.__metadata = []
            
            self.__metadata.append(Metadata(format=u'marc21.bibliographic',
                                            source=self.getSource(),
                                            value=self.getMarcRecord()))
            
            holdings = set()
            
            for holding in self.holdings:
                if not holding.id in holdings and not holding.suppressed():
                    self.__metadata.append(Metadata(format=u'marc21.holdings',
                                                    source=self.getSource(),
                                                    value=holding.getMarcRecord()))
                    holdings.add(holding.id)
            
            self.__metadata.append(Metadata(format=u'voyager',
                                            source=self.getSource(),
                                            value=self.getVoyagerMetadata()))
            
            return self.__metadata
        
        def getResource(self):
            return None
        
        def getLastModified(self):
            """Return a datetime object for the last time that this instance was 
            modified.
            """
            dates = []
            
            if self.created:
                dates.append(self.created)
            
            if self.modified:
                dates.append(self.modified)
            
            for holding in self.holdings:
                if not holding.suppressed():
                    if holding.created:
                        dates.append(holding.created)
                    if holding.modified:
                        dates.append(holding.modified)
                    
                    for item in holding.items:
                        if item.item.created:
                            dates.append(item.item.created)
                        if item.item.modified:
                            dates.append(item.item.modified)
                        
                        for change in item.item.status:
                            if change.date:
                                dates.append(change.date)
            
            return max(dates)
        
        def getMarcRecord(self):
            self.marc.sort(key=lambda s: s.seqnum)
            
            return unicode('','utf-8').join(segment.data for \
                                            segment in self.marc)
        
        def getVoyagerMetadata(self):
            """Create an XML fragment with Voyager specific metadata """
            
            holdings = set()
            
            rdf = etree.Element('{%s}RDF' % (NAMESPACES['rdf'],),
                                nsmap=NAMESPACES)
            
            work = etree.Element('{%s}Manifestation' % (NAMESPACES['frbr'],),
                                 attrib={'{%s}about' % (NAMESPACES['rdf'],): 
                                           '%s/bibliography/%s' % (self.getSource(),
                                                             str(self.id),)},
                                 nsmap=NAMESPACES)
            
            id = etree.Element('{%s}identifier' % (NAMESPACES['dc'],),
                               attrib={'type':u'voyagerBib'})
            id.text = str(self.id)
            work.append(id)
            
            if self.library:
                work.append(self.library.getXML())
            
            for holding in self.holdings:
                if not holding.suppressed():
                    x = holding.getXML()
                    if x is not None:
                        work.append(x)
            
            return etree.tounicode(work)
    
    return {'VoyagerBibData':VoyagerBibData,
            'VoyagerBibRecord':VoyagerBibRecord,
            'VoyagerHolding':VoyagerHolding,
            'VoyagerHoldingData':VoyagerHoldingData,
            'VoyagerHoldingItems':VoyagerHoldingItems,
            'VoyagerItem':VoyagerItem,
            'VoyagerItemStatus':VoyagerItemStatus,
            'VoyagerItemStatusType':VoyagerItemStatusType,
            'VoyagerLibrary':VoyagerLibrary,
            'VoyagerLocation':VoyagerLocation}
